16.6 抽象並行GC
並行GCには共通する項目が多くある一方で、細部が細かく異なったりしているので、それをわかりやすくするために抽象的なフレームワークを導入する。
並行コレクタの正当性は、コレクタとミューテータの協調に依存するので、それらのイベントをログに記録する。
以下のイベントを記録する。
T<src, fld, old, new>: srcオブジェクトのfldフィールドをTraceしたことの記録。そこに入っていた参照をoldからnewへと置き換えたことを示す。
N<ref>: refを割り付けたこと(New)。
R<src, fld, old>: Readの記録。srcのfldから値oldを読んだこと。
W<src, fld, old, new>: Write。読み方はTと同じ。
非移動型コレクタでは、追跡しても参照を書き換えないため、Tイベントについてold = newである。
以下のアルゴリズム16-4を例として説明する。アルゴリズム6-1(P95 6.6)の拡張で、コレクタとミューテータが並行に動作できるようにしたもの。
Wって何かと思ったらワークリストらしい。
rhoは参照カウント(本だとρ)
exposeはログを受け取り、オブジェクトの集合を返す。これを変えることがアルゴリズムを変えることと対応するらしい。
code:16-4.py
shared log = []
def collectTracingInc():
atomic
rootsTracing(W) # ミューテータがrootを途中で書き換えることとか考えるとややこしくなるので、ここはatmicにする。
log = []
while <?>: # 簡単には条件が書けなかったりするので、ごまかしている?(exposeと協調した何かをするかもしれない)
scanTracingInc(W)
addOrigins(W)
atomic
addOrigins(W)
scanTracingInc(W)
sweepTracing() # 未定義。うまいことこの中でもログを吐く必要がある。
def scanTracingInc(W):
while not isEmpty(W):
src = remove(W)
if rho(src) == 0:
for each fld in Pointers(src):
atomic
ref = *fld
if ref != null
rho(src) = rho(src) + 1 # ここで参照カウントを増やしているのは、rhoはコレクタに何回見られたかどうかを示していて、参照カウンタの実装だからである(rhoという便利なものが外にあるのではなく、自分がこのカウントをしている)。
def addOrigins(W):
atomic
origins = expose(log) # exposeは関数パラメタ
for each src in origins
def New():
ref = allocate()
atomic
rho(ref) = 0
return ref
atomic def Write(src, i, new):
if src != roots:
log = log + [W<src, &srci, old, new>] <?>にて非決定的な選択をして、終了を保証する。全ての実装で必要というわけではない。
コレクタの境界
ログは、コレクタがどこまでヒープを辿ったかを記録する。境界をTイベントという形で記録する。
その間に起こるミューテータイベント(T以外)は、境界(Tイベントにより定まる)より向こう側かどうかで扱いが変わる。
N, R, Wしたのが黒/灰/白のどちらなのかを判定するためにTが使われる。
実装上はこの境界をさまざまな精度で近似することがあり、いろいろな精度がある。
開始点の追加
addOriginsは、logを使い、生きているとみなすオブジェクトを選択する。expose関数が新たなワークリストを発見するということか。
ミューテータバリア
New, Writeはミューテータバリアの実装である。
New objをlogに記録することで今までライト/リードバリアで実装していたことをexposeの実装を使って表現できるようになった。
New直後のオブジェクトは、1箇所からのみ参照されていて、他のオブジェクトを指していない。
コレクタによっては、このようなオブジェクトは到達可能性にかかわらず生きていると扱い、次のサイクルまで回収と待つこともある。
exposeの実装の差でこういう違いを吸収できるようになった。
Write操作によりsrc[i] = newの代入が発生し、newへの参照が発生すると共に、oldのものは消える。コレクタの境界の後ろにsrc[i]が位置するなら、「境界の背後で挿入/削除」されると言い、そうでないなら「前方で挿入/削除される」と呼ぶ。
境界の前方のオブジェクトは三色抽象化でいう白、境界上のものは灰色、背後のものは黒である。
精度
不可分性のレベルは固定されているが、精度についてはexposeの実装を変えることにより変わる。これの実装をとりかえることで、代表的な並行コレクタについての特徴を記述できる。
もちろん16-4の形で書けないコレクタも存在する。
16-4では、ルートが不可分に取れることを仮定しているので、ミューテータスレッドを止めて抽出することが必要であるようなものである。これはつまり準並行である。
コレクタの具体化
exposeを定義していく。
Steeleスタイル(アルゴリズム15-1 (a))の、境界と境界の背後の全オブジェクトを再走査する並行コレクタでは、境界はオブジェクトごとの最後のTレコードからわかる。 srcが同じTレコードの列から、srcが同じWレコードより以前にあるTレコードを除いた列に対して、
その列が空のとき、srcは白
その列に含まれるfldがsrcのフィールドを尽くしているとき、srcは黒
どちらでもないとき、srcは灰
よって、ここでのexposeは、srcが黒でnewが白のWレコードたちを返す
Dijkstraのもの(アルゴリズム15-1 (c))では、logに含まれる全てのTレコードのsrcと、任意のTレコードと(srcとfldが)マッチする過去の(そのマッチ以前の)Wレコードのnewを返す。